package com.nf.mvc.exception;

import com.nf.mvc.HandlerExceptionResolver;
import com.nf.mvc.ViewResult;
import com.nf.mvc.handler.HandlerMethod;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;

import static com.nf.mvc.MvcContext.getMvcContext;
import static com.nf.mvc.ViewResult.adaptHandlerResult;
import static com.nf.mvc.util.AnnotationUtils.getAttrValue;
import static com.nf.mvc.util.ExceptionUtils.compare;
import static com.nf.mvc.util.ExceptionUtils.getRootCause;

/**
 * 这是mvc框架主用的一个异常解析器，此异常解析器支持用ExceptionHandler注解修饰的方法来处理执行链异常.
 * <h3>典型用法</h3>
 * <p>
 * 用户通常编写一个单独的类，此类里面每一个方法用来处理某一类异常，代码如下：
 * <pre class="code">
 *      public class GlobalExceptionHandler{
 *          &#064;ExceptionHandler(RuntimeException.class)
 *          public JsonViewResult handleRuntime(RuntimeException re){
 *               System.out.println("re = " + re);
 *              return new JsonViewResult(new ResponseVO(10001,"订单超时。。","runtime--"));
 *          }
 *
 *          &#064;ExceptionHandler(ArithmeticException.class)
 *          public JsonViewResult handleArithmeticException(ArithmeticException re){
 *              System.out.println("arithmetic = " + re);
 *              return new JsonViewResult(new ResponseVO(10002,"某个描述","算数--"));
 *          }
 *      }
 *  </pre>
 * </p>
 *
 * <p>
 * 如果一个异常有多个异常处理方法都是可以处理的，那么会用ExceptionHandler注解中指定的能处理类型最接近(指的是继承链条上)引发的异常的方法来处理异常，
 * 比如引发了一个ArithmeticException，那么上面的示例代码中handleArithmeticException方法会被用来处理异常，
 * 如果类中没有能处理ArithmeticException异常的方法，那么就会交给handleRuntime方法去处理
 * </p>
 * <p>
 * 这些异常处理方法必须有一个参数，并且类型只能是Exception类型或其子类型，
 * 并不能像Handler那样支持各种类型，你如果有兴趣可以改写此类的实现来达成此目的
 * 其方法返回值与handler的方法返回值是一样的，可以是void，ViewResult或其它类型，
 * 异常解析器负责把异常处理方法的返回结果适配为ViewResult类型
 * </p>
 * <h3>核心思想</h3>
 *   <ul>
 *     <li>找到异常处理方法:{@link #resolveExceptionHandlerMethods()}
 *          <ol>
 *              <li>实例化时扫描所有符合规则的异常处理方法，见{@link #scanExceptionHandlerMethods(Predicate)}</li>
 *              <li>如果要对第一步扫描到的方法进行额外的后置处理就执行{@link #postHandleExceptionHandlerMethods(List)}方法</li>
 *           </ol>
 *     </li>
 *     <li>处理用户引发的异常:{@link #resolveException(HttpServletRequest, HttpServletResponse, Object, Exception)} ()}
 *          <ol>
 *            <li>找出真正的异常:{@link #getRootException(Exception)} </li>
 *            <li>校验这个真正的异常是否是能处理的异常{@link #validateRootException(Throwable)}</li>
 *            <li>找出最匹配的异常处理方法：{@link #findMostMatchedHandlerMethod(List, Exception)} </li>
 *            <li>执行异常处理方法以处理执行链引发的异常:{@link #executeExceptionHandlerMethod(HandlerMethod, Exception, HttpServletRequest)}</li>
 *          </ol>
 *     </li>
 *   </ul>
 *   <h3>扩展建议</h3>
 *   子类通常只需要重写以下几个方法中的某一些即可：
 *   <ul>
 *       <li>{@link #scanExceptionHandlerMethods(Predicate)}</li>
 *       <li>{@link #postHandleExceptionHandlerMethods(List)}</li>
 *       <li>{@link #isExceptionHandleMethod(Method)}</li>
 *       <li>{@link #getRootException(Exception)}</li>
 *       <li>{@link #validateRootException(Throwable)}</li>
 *       <li>{@link #findMostMatchedHandlerMethod(List, Exception)}</li>
 *       <li>{@link #executeExceptionHandlerMethod(HandlerMethod, Exception, HttpServletRequest)}</li>
 *   </ul>
 *   而不建议重写:
 *   <ul>
 *       <li>{@link #resolveExceptionHandlerMethods()}</li>
 *       <li>{@link #resolveException(HttpServletRequest, HttpServletResponse, Object, Exception)}</li>
 *       <li>{@link #resolveExceptionInternal(HttpServletRequest, HttpServletResponse, Object, Exception)}</li>
 *   </ul>
 *   <p>所以这三个方法是可以设置为final的，但我只给{@link #resolveExceptionInternal(HttpServletRequest, HttpServletResponse, Object, Exception)}设置为了final，
 *   这是为了灵活性考虑的，因为重写了{@link #resolveExceptionHandlerMethods()}
 *   与{@link #resolveException(HttpServletRequest, HttpServletResponse, Object, Exception)}方法相当于彻底改变了整个异常解析器的扫描以及处理逻辑</p>
 *
 * <h3>线程安全性</h3>
 * <p>此类是一个单例对象，并且在{@link com.nf.mvc.DispatcherServlet}类的初始化阶段就已经完成实例化，
 * 所以，此类对异常处理方法的解析只有一次，后续在多线程环境下对方法{@link #resolveException(HttpServletRequest, HttpServletResponse, Object, Exception)}
 * 也不会存在线程不安全的问题</p>
 * <p><b><i>注意:整个框架的所有异常解析器主要关注的是异常处理方法上注解指定的异常,对于挂羊头卖狗肉的情况没有进行特别严谨的处理,
 * 同学们可以自行解决这个问题.挂羊头卖狗肉指的是注解指定能处理A异常,结果方法参数指定的是B异常类型,A与B两个异常类型又没有is-a关系</i></b></p>
 *
 * @see com.nf.mvc.DispatcherServlet
 * @see HandlerExceptionResolver
 * @see ExceptionHandler
 * @see ExceptionHandlersExceptionResolver
 * @see ParameterizedExceptionHandlersExceptionResolver
 */
public class ExceptionHandlerExceptionResolver implements HandlerExceptionResolver {
    /**
     * 子类不要直接访问此字段，通过对应的getter方法来访问此字段，
     * 这里用HandlerMethod类型而不用Method类型是为了以后可能的功能增强，
     * 比如支持的参数与Handler一致(RequestMappingHandlerAdapter 处理的handler就是HandlerMethod)，
     * 而不是像此类实现一样只支持一个Exception类型的参数
     */
    private final List<HandlerMethod> exceptionHandlerMethods = new ArrayList<>();

    public ExceptionHandlerExceptionResolver() {
        resolveExceptionHandlerMethods();
    }

    /**
     * 这个方法就是用来找到异常处理方法以及对找到的这些异常处理方法进行后置处理,
     * 通常的后置处理是对这些异常处理方法基于其能处理的异常进行排序处理.
     * <p>比如有下面的两个异常处理方法,都能对ArithmeticException异常进行处理,那么更合适的就是m1,进行后置处理之后,m1就排在m2的前面
     * <pre class="code">
     *    &#064;ExceptionHandler(ArithmeticException ex)
     *    m1(ArithmeticException ex){},
     *
     *    &#064;ExceptionHandler( RuntimeException ex)
     *    m2(RuntimeException ex){}
     * </pre>
     * </p>
     * <p>
     * 这个方法被设计成protected，是为了可以被继承，以便改写ExceptionHandlerExceptionResolver的异常处理逻辑，
     * 比如支持其它的注解或者有其它的排序算法等。给scanExceptionHandlerMethods与
     * 方法postHandleExceptionHandlerMethods设计为有参数的形式是为了增加灵活性，
     * 比如下面的重写方法就改变成了方法上有ExHandler注解的才是异常处理方法
     * <pre class="code">
     *         &#064;Override
     *         protected void resolveExceptionHandlerMethods(){
     *             scanExceptionHandlerMethods(method->method.isAnnotationPresent(ExHandler.class));
     *             postHandleExceptionHandlerMethods(this.exHandleMethods);
     *         }
     *      </pre>
     * 而如果重写了scanExceptionHandlerMethods方法，那么可以完全改写扫描获取异常处理的逻辑，具有更大的灵活性,
     * 子类可以通过调用方法{@link #getExceptionHandlerMethods()}来获取所有的异常处理方法的集合类型,
     * 这样子类在编写自定义异常方法扫描逻辑时就可以把找到的处理方法添加到这个集合中，以便后续的方法使用，
     * 比如postHandleExceptionHandlerMethods
     * </p>
     */
    protected void resolveExceptionHandlerMethods() {
        scanExceptionHandlerMethods(this::isExceptionHandleMethod);
        postHandleExceptionHandlerMethods(this.exceptionHandlerMethods);
    }

    /**
     * 此方法解析出所有的异常处理方法，至于什么是异常处理方法由方法{@link #isExceptionHandleMethod(Method)}决定
     *
     * @param predicate:判断条件，此谓词返回true的才是有效的异常处理方法
     */
    protected void scanExceptionHandlerMethods(Predicate<Method> predicate) {
        Set<Class<?>> classList = getMvcContext().getAllScannedClasses();

        for (Class<?> clz : classList) {
            Method[] methods = clz.getDeclaredMethods();
            for (Method method : methods) {
                if (predicate.test(method)) {
                    HandlerMethod exHandleMethod = new HandlerMethod(method);
                    exceptionHandlerMethods.add(exHandleMethod);
                }
            }
        }
    }

    /**
     * 这个方法是用来对所有的异常处理方法进行后置处理用的，常见的处理是对异常处理方法基于其能处理的异常进行排序，
     * 也可以不对这些异常处理方法进行任何额外的处理，子类可以通过重写此方法并留空或者重写{@link #resolveExceptionHandlerMethods()}
     * 但只调用{@link #scanExceptionHandlerMethods(Predicate)}的方式来达成不对异常处理方法进行额外处理的效果
     *
     * @param exHandlerMethods 异常处理方法集合
     */
    protected void postHandleExceptionHandlerMethods(List<HandlerMethod> exHandlerMethods) {
        exHandlerMethods.sort((m1, m2) -> compare(
                getAttrValue(m1.getMethod(), ExceptionHandler.class),
                getAttrValue(m2.getMethod(), ExceptionHandler.class)));
    }

    /**
     * 用来检查一个方法是否是一个异常处理方法
     * <p>默认实现是方法上只要有注解{@link ExceptionHandler}就认为是一个异常处理方法，
     * 你也可以更改这个默认实现，比如更改为方法只能有一个参数，并且参数的类型与{@link ExceptionHandler}里指定的异常类型兼容，
     * 比如下面的实现就解决了兼容问题，也就是解决了我称之为的挂羊头卖狗肉的问题
     * <pre class="code">
     *         protected boolean isExceptionHandleMethod(Method method) {
     *              return method.isAnnotationPresent(ExceptionHandler.class)
     *                 && method.getParameterCount()==1
     *                 && Exception.class.isAssignableFrom( method.getParameters()[0].getType());
     *     }
     * </pre>
     * </p>
     *
     * @param method 扫描到的异常处理类中声明的各个方法
     * @return 是异常处理方法就返回true，反之返回false
     */
    protected boolean isExceptionHandleMethod(Method method) {
        return method.isAnnotationPresent(ExceptionHandler.class);
    }

    @Override
    public ViewResult resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        Throwable rootCause = getRootException(ex);
        if (!validateRootException(rootCause)) {
            // 表明真正的异常不是一个Exception异常，此异常解析器不能处理，返回null交给链的下一个异常解析器处理
            return null;
        }

        return resolveExceptionInternal(request, response, handler, (Exception) rootCause);
    }

    public final ViewResult resolveExceptionInternal(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        HandlerMethod exceptionHandlerMethod = findMostMatchedHandlerMethod(getExceptionHandlerMethods(), ex);
        if (exceptionHandlerMethod == null) {
            return null;
        }
        try {
            Object exceptionHandlerResult = executeExceptionHandlerMethod(exceptionHandlerMethod, ex, request);
            return adaptHandlerResult(exceptionHandlerResult);
        } catch (Exception e) {
      /* 进入到这里就是异常处理方法本身的执行出了错，catch里如果什么都不干，相当于吞掉异常处理方法本身的异常;
       异常处理方法本身执行出问题其含义就是说本异常解析器无法处理异常.因此，通过在catch这里返回null的形式，
       就继续交给下一个异常解析器去处理，下一个异常解析器处理的仍然是最开始抛出的异常，也就是这个方法被调用时传递进来的第四个参数的值 */
            return null;
        }
    }

    /**
     * 抛出的异常是Exception，但真正的异常可能是一个Throwable类型的
     *
     * @param ex 执行链执行过程中抛出的异常，可能是InvocationTargetException类型
     * @return 执行链真正的异常
     */
    protected Throwable getRootException(Exception ex) {
        return getRootCause(ex);
    }

    /**
     * 校验执行链抛出的异常是否能被本异常解析器处理，返回true表示是本异常解析器能处理的异常，
     * 会调用{@link #resolveExceptionInternal(HttpServletRequest, HttpServletResponse, Object, Exception)}进行后续的异常处理，
     * 此方法返回false表明本异常解析器不能处理执行链抛出的异常，要由链中的下一个异常解析器去处理。
     * <p>当前的规则是异常只要是Exception类型就可以被本异常解析器处理</p>
     *
     * @param exception 执行链真正的异常
     * @return 是解析器能处理的异常就返回true，否则返回false
     */
    protected boolean validateRootException(Throwable exception) {
        return exception instanceof Exception;
    }

    /**
     * 此方法是用来找出能对执行链异常进行处理的最合适的异常处理方法,通常是在{@link #postHandleExceptionHandlerMethods(List)}
     * 方法调用之后才执行本方法,比如{@link #postHandleExceptionHandlerMethods(List)}已经依据其能处理的异常进行了排序,
     * 那么本方法的实现只只需要找到第一个异常处理方法就可以了,不需要再有额外的逻辑处理,下面的方法就是这样实现的
     *
     * @param handlerMethods 异常处理方法的集合
     * @param exception      当前用户代码引发的异常
     * @return 找到的能处理当前用户异常的HandlerMethod
     * @author cj
     */
    protected HandlerMethod findMostMatchedHandlerMethod(List<HandlerMethod> handlerMethods, Exception exception) {
        HandlerMethod matchedHandlerMethod = null;
        for (HandlerMethod handlerMethod : handlerMethods) {
            Method method = handlerMethod.getMethod();
            Class<?> exceptionClass = getAttrValue(method, ExceptionHandler.class);
            if (exceptionClass.isAssignableFrom(exception.getClass())) {
                matchedHandlerMethod = handlerMethod;
                break;
            }
        }
        return matchedHandlerMethod;
    }

    protected Object executeExceptionHandlerMethod(HandlerMethod exceptionHandlerMethod, Exception raisedException, HttpServletRequest request) throws Exception {
        Method method = exceptionHandlerMethod.getMethod();
        Object instance = exceptionHandlerMethod.getHandlerObject();
        return method.invoke(instance, raisedException);
    }

    protected List<HandlerMethod> getExceptionHandlerMethods() {
        return exceptionHandlerMethods;
    }
}
